﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.OleDb;
using System.Data;
using System.Xml.Serialization;
using System.IO;
using System.Dynamic;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using System.Threading;

namespace RazorSourceGenerator
{
    public class Generator
    {
        public static event Action<string> LogReport;
        public const string DEFAULT_CONFIG_NAME = "Sample.config";

        private GeneratorConfig _config;
        
        private CancellationToken _cancelToken;
        private string _configFileName;
        private DataTable _globalParams;

        public DataTable GlobalParams
        {
            get
            {
                return _globalParams;
            }
            set
            {
                _globalParams = value;
            }
        }
        
        private Dictionary<string, string> _templatePools = new Dictionary<string, string>();

        static Generator()
        {
            LogReport += (msg) => Console.WriteLine(msg);
        }

        public Generator(string configFileName, GeneratorConfig config)
        {
            _config = config;
            _configFileName = configFileName;
        }

        public Task RunAsync(CancellationToken cancelToken)
        {
            _cancelToken = cancelToken; 
            return new Task(Run, cancelToken);
        }

        public void TestConnection()
        { 
            var connectionString = _config.ConnectionString;
            using (var conn = new OleDbConnection(connectionString))
            {
                conn.Open();
            }
        }

        public void Run()
        {
            var stopWatch = new Stopwatch();
            stopWatch.Start();
            try
            {
                if (!Directory.Exists(_config.WorkDir))
                    Directory.CreateDirectory(_config.WorkDir);

                _templatePools = new Dictionary<string, string>();

                OnLogReport(string.Format("[{0}]", System.IO.Path.GetFileName(_configFileName)));
                OnLogReport("Generate Begin >>>");
                OnLogReport(_config.ConnectionString);

                var enabledTemplates = _config.Templates.Where(t => t.Enabled && !t.IsIncluding);

                var genBySqlTemplates = enabledTemplates.Where(t => !string.IsNullOrEmpty(t.SQL));
                if (genBySqlTemplates.Count() > 0)
                {
                    OnLogReport("--GenBySQL Begin--");
                    GenBySql(genBySqlTemplates);
                    OnLogReport("--GenBySQL End--");
                }

                var genBySchemaTemplates = enabledTemplates.Where(t => string.IsNullOrEmpty(t.SQL));
                if (genBySchemaTemplates.Count() > 0)
                {
                    OnLogReport("--GenBySchema Begin--");
                    GenBySchema(genBySchemaTemplates);
                    OnLogReport("--GenBySchema End--");
                }
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                OnLogReport("Error: " + ex.Message);
            }
            finally
            {
                stopWatch.Stop();
                OnLogReport("Generate End <<<");
                OnLogReport(string.Format("[{0}s] was executed.", (double)stopWatch.ElapsedMilliseconds / 1000));
            }
        }

        private void CheckCancellation()
        {
            if (_cancelToken != null && _cancelToken.IsCancellationRequested)
            {
                _cancelToken.ThrowIfCancellationRequested();
            }
        }

        private void GenBySql(IEnumerable<TemplateConfig> templates)
        {
            dynamic parseContext = new ExpandoObject();
            foreach (var template in templates)
            {
                CheckCancellation();
                try
                {
                    ParseAndOutputBySql(template, parseContext);
                    OnLogReport(string.Format("  {0} OK", template.Name));
                }
                catch (Exception ex)
                {
                    OnLogReport("Error: " + ex.Message);
                }
            }
        }

        private void GenBySchema(IEnumerable<TemplateConfig> templates)
        {
            var errorTableNames = new List<string>();
            try
            {
                CheckCancellation();

                var dbSchemaTable = GetDbSchema();
                OnLogReport("GetDbSchema Successfully.");

                var tables = dbSchemaTable.AsEnumerable();
                if (!string.IsNullOrEmpty(_config.TableFilter))
                {
                    tables = tables.Where(r => Regex.IsMatch(r["TABLE_NAME"].ToString(), _config.TableFilter));
                }
                if (!string.IsNullOrEmpty(_config.TableNames))
                {
                    var tableNames = _config.TableNames.Split(',');
                    tables = tables.Where(r => tableNames.Contains(r["TABLE_NAME"].ToString()));
                }

                dynamic context = new ExpandoObject();
                int index = 0;
                int count = tables.Count();
                context.Count = count;

                foreach (var table in tables)
                {
                    var tableName = table["TABLE_NAME"].ToString();
                    context.Index = index;                   
                    foreach (var template in templates)
                    {
                        CheckCancellation();
                        if (index == 0)
                        {
                            context.IsFirst = true;
                            context.IsLast = false;
                        }
                        else if (index == count - 1)
                        {
                            context.IsFirst = false;
                            context.IsLast = true;
                        }
                        else
                        {
                            context.IsFirst = false;
                            context.IsLast = false;
                        }

                        try
                        {
                            if (template.Enabled && !template.IsIncluding)
                            {
                                ParseAndOutputBySchema(tableName, template, context);
                                OnLogReport(string.Format("  {0} -> {1} OK", tableName, template.Name));
                            }
                        }
                        catch (Exception err)
                        {
                            OnLogReport(string.Format("  {0} -> {1} ERR:{2}", tableName, template.Name, err.Message));
                            errorTableNames.Add(tableName);
                        }
                    }
                    index++;
                }
            }
            finally
            {
                if (errorTableNames.Count > 0)
                {
                    OnLogReport("--- Error Table ---");
                    errorTableNames.ForEach(OnLogReport);
                }
            }
        }

        private void SetGlobalParameters(dynamic model)
        {
            if (model != null && _globalParams != null)
            {
                var dict = (IDictionary<string, object>)model;
                foreach (DataRow row in _globalParams.Rows)
                {
                    var name = row["NAME"].ToString();
                    if (!string.IsNullOrEmpty(name))
                    {
                        dict[name] = row["VALUE"] as string;
                    }
                }
            }
        }

        private void OnLogReport(string msg)
        {
            if (LogReport != null)
                LogReport(msg);
        }

        public DataTable GetDbSchema(bool allSchema = false)
        {
            var connectionString = _config.ConnectionString;
            using (var conn = new OleDbConnection(connectionString))
            {
                conn.Open();

                var catalog = string.IsNullOrEmpty(_config.TableCatalog) ? null : _config.TableCatalog;
                var schema = string.IsNullOrEmpty(_config.TableSchema) ? null : _config.TableSchema;
                var tableType = string.IsNullOrEmpty(_config.TableType) ? null : _config.TableType;
                _config.TableNames = _config.TableNames ?? "";

                var dataOfTable = conn.GetOleDbSchemaTable(OleDbSchemaGuid.Tables,
                                        new object[] { catalog, schema, null, "TABLE" });
                var dataOfView = conn.GetOleDbSchemaTable(OleDbSchemaGuid.Tables,
                                    new object[] { catalog, schema, null, "VIEW" });
                dataOfTable.Merge(dataOfView, true);

                return dataOfTable;
            }
        }

        private void ClearOutputDir(bool clear, string outputFileName)
        {
            if (clear)
            {
                string dir = Path.GetDirectoryName(outputFileName);
                if (Directory.Exists(dir))
                {
                    string[] files = Directory.GetFiles(dir);
                    files.Where(f => f != _configFileName).ToList().ForEach(File.Delete);
                }
                //files.Where(f => f != outputFileName).ToList().ForEach(File.Delete);
            }
        }

        private void ParseAndOutputBySchema(string tableName, TemplateConfig templateConfig, dynamic context)
        {
            dynamic model = new ExpandoObject();
            SetGlobalParameters(model);
            model.TableName = tableName;
            model.Columns = GetColumnSchemas(tableName);
            model.Context = context;            
            model.TableFileName = GeneratorUtils.ConvName(model.TableName);
            var result = ParseTemplate(templateConfig.FileName, model);
            var outputFileName = ParseFileName(templateConfig.OutputFileName, model);
            if (context.IsFirst)
                ClearOutputDir(templateConfig.ClearOutputDir, outputFileName);
            OutputResult(outputFileName, templateConfig.Encoding, result, templateConfig.Append, context);
        }

        private void ParseAndOutputBySql(TemplateConfig templateConfig, dynamic context)
        {
            dynamic model = new ExpandoObject();
            SetGlobalParameters(model);
            try
            {
                model.Result = GetSQLResult(templateConfig.SQL);
            }
            catch (Exception ex)
            {
                throw new Exception("[SQLError]" + ex.Message, ex);
            }
            int index = 0;
            context.Count = model.Result.Count;            
            if (templateConfig.LoopInSQLResult)
            {
                foreach (dynamic row in model.Result)
                {
                    context.Index = index;
                    if (index == 0) 
                    {
                        context.IsFirst = true;
                        context.IsLast = false;
                    }
                    else if (index == model.Result.Count - 1)
                    {
                        context.IsFirst = false;
                        context.IsLast = true;
                    }
                    else
                    {
                        context.IsFirst = false;
                        context.IsLast = false;
                    }
                    row.Context = context;
                    var result = ParseTemplate(templateConfig.FileName, row);
                    var outputFileName = ParseFileName(templateConfig.OutputFileName, row);
                    if (context.IsFirst)
                        ClearOutputDir(templateConfig.ClearOutputDir, outputFileName);
                    OutputResult(outputFileName, templateConfig.Encoding, result, templateConfig.Append, context);
                    index++;
                }
            }
            else
            {
                context.Index = 0;
                context.IsFirst = true;
                context.IsLast = true;
                model.Context = context;
                var result = ParseTemplate(templateConfig.FileName, model);
                var outputFileName = ParseFileName(templateConfig.OutputFileName, model);
                ClearOutputDir(templateConfig.ClearOutputDir, outputFileName);
                OutputResult(outputFileName, templateConfig.Encoding, result, templateConfig.Append, context);
            }
        }

        private void OutputResult(string outputFileName, string encoding, string result, bool append, dynamic context)
        {
            var dir = Path.GetDirectoryName(outputFileName);
            if (!Directory.Exists(dir))
                Directory.CreateDirectory(dir);

            result = result.Trim('\r', '\n');
            if (!context.IsLast)
                result += "\r\n";

            Encoding encodingObj = Encoding.Default;
            
            encoding = string.IsNullOrEmpty(encoding) ? "utf-8" : encoding.ToLower();
            if (encoding.StartsWith("utf-8"))
            {
                if (encoding == "utf-8 without signature")
                    encodingObj = new UTF8Encoding(false);
                else if (encoding == "utf-8 with signature")
                    encodingObj = new UTF8Encoding(true);
                else
                    encodingObj = Encoding.UTF8;
            }
            else {
                encodingObj = Encoding.GetEncoding(encoding);
            }

            if (append)
                File.AppendAllText(outputFileName, result, encodingObj);
            else
                File.WriteAllText(outputFileName, result, encodingObj);
        }

        private string ParseTemplate(string templateFileName, dynamic model)
        {
            string template = GetTemplate(templateFileName);
            template = ReplaceIncludeTemplate(templateFileName, template);
            return RazorEngine.Razor.Parse(template, model);
        }

        private string ReplaceIncludeTemplate(string templateFileName, string template)
        {
            var regex = new Regex(@"(?is)\$\{include\(([^\{\}]*?)\)\}");
            var hasReplacedAction = false;
            var replacedTemplate = regex.Replace(template, new MatchEvaluator(m =>
            {
                if (m.Groups.Count < 2)
                    return m.Groups[0].Value;
                hasReplacedAction = true;
                var includedTemplateFileName = m.Groups[1].Value;
                var includedTemplate = GetTemplate(includedTemplateFileName);
                return includedTemplate;
            }));
            if (hasReplacedAction)
                _templatePools[templateFileName] = replacedTemplate;
            return replacedTemplate;
        }

        private string GetTemplate(string templateFileName)
        {
            string template = "";
            if (!_templatePools.ContainsKey(templateFileName))
            {
                template = File.ReadAllText(GeneratorUtils.GetActualFileName(_config.WorkDir, templateFileName));
                _templatePools[templateFileName] = template;
            }
            else
                template = _templatePools[templateFileName];
            return template;
        }

        private string ParseFileName(string fileName, dynamic model)
        {
            var dict = (IDictionary<string, object>)model;
            var regex = new Regex(@"(?<=\{)[\w\d_]+(?=\})");
            var matches = regex.Matches(fileName);
            foreach (Match m in matches)
            { 
                var key = m.Groups[0].Value;
                var name = "{" + key + "}";
                fileName = fileName.Replace(name, dict[key] as string ?? key);
            }
            return GeneratorUtils.GetActualFileName(_config.WorkDir, fileName);
        }

        private IEnumerable<dynamic> GetSQLResult(string sql)
        {
            using (var conn = new OleDbConnection(_config.ConnectionString))
            {
                using (var comm = new OleDbCommand())
                {
                    comm.Connection = conn;
                    comm.CommandText = sql;
                    conn.Open();
                    using (var reader = comm.ExecuteReader())
                    {
                        var result = new List<dynamic>();
                        while (reader.Read())
                        {
                            dynamic item = new ExpandoObject();
                            var dict = (IDictionary<string, object>)item;
                            for (var i = 0; i < reader.FieldCount; i++)
                            {
                                var obj = reader.GetValue(i) ?? "";
                                dict[reader.GetName(i)] = Convert.IsDBNull(obj) ? "" : Convert.ToString(obj);
                            }
                            result.Add(item);
                        }
                        return result;
                    }

                }
            }
        }
  
        public DataTable GetColumnSchemasBase(string tableName)
        {
            using (var conn = new OleDbConnection(_config.ConnectionString))
            {
                using (var comm = new OleDbCommand())
                {
                    comm.Connection = conn;
                    var provider = conn.Provider.ToUpper();

                    if (provider.Contains("SQLOLEDB"))
                    {
                        comm.CommandText = "select * from [" + tableName + "] where 1=2";
                    }
                    else
                    {
                        comm.CommandText = "select * from " + tableName + " where 1=2";
                    }

                    conn.Open();
                    DataTable comments = GetColumnComments(conn, tableName);

                    using (var reader = comm.ExecuteReader(CommandBehavior.KeyInfo))
                    {
                        var table = reader.GetSchemaTable();
                        var descriptionCol = table.Columns.Add("Description", typeof(string));

                        foreach (DataRow row in table.Rows)
                        {
                            if (comments != null && comments.Rows.Count > 0)
                            {
                                var commentRow = comments.AsEnumerable().FirstOrDefault(r => r["COLUMN_NAME"].ToString() == row["ColumnName"].ToString());
                                if (commentRow != null)
                                    row[descriptionCol] = commentRow["COMMENTS"] as string;
                            }
                        }
                        return table;
                    }
                }
            }
        }

        public IEnumerable<dynamic> GetColumnSchemas(string tableName)
        {
            var tableSchema = GetColumnSchemasBase(tableName);
            return GeneratorUtils.ConvertTableToDynamicCollection(tableSchema);
        }

        private DataTable GetColumnComments(OleDbConnection conn, string tableName)
        {
            DataTable comments = null;
            var provider = conn.Provider.ToUpper();
            if (provider.Contains("ORACLE") || provider.Contains("MSDAORA"))
            {
                comments = GetOracleColumnExtensionInfo(conn, tableName);
            }
            else if (provider.Contains("SQLOLEDB"))
            {
                comments = GetSqlServerExtensionInfo(conn, tableName);
            }
            else if (provider.Contains("MICROSOFT.JET.OLEDB"))
            {
                comments = RazorSourceGeneratorADOX.ADOXUtils.GetColumnsDescription(conn.ConnectionString, tableName);
            }
            return comments;
        }

        private DataTable GetOracleColumnExtensionInfo(OleDbConnection conn, string tableName)
        {
            var sql = "select COLUMN_NAME, COMMENTS from sys.all_col_comments where owner = '{0}' and table_name = '{1}'";
            var schema = string.IsNullOrEmpty(_config.CommentSchema) ? _config.TableSchema : _config.CommentSchema;
            sql = string.Format(sql, schema, tableName);
            var adapter = new OleDbDataAdapter(sql, conn);
            var data = new DataTable();
            adapter.Fill(data);
            return data;
        }

        private DataTable GetSqlServerExtensionInfo(OleDbConnection conn, string tableName)
        {
            var sql = "select objname as COLUMN_NAME, cast(value as varchar) as COMMENTS from sys.fn_listextendedproperty(NULL, 'user', '{0}', 'table', '{1}', 'column', NULL)";
            var schema = string.IsNullOrEmpty(_config.CommentSchema) ? _config.TableSchema : _config.CommentSchema;
            sql = string.Format(sql, schema, tableName);
            var adapter = new OleDbDataAdapter(sql, conn);
            var data = new DataTable();
            adapter.Fill(data);
            return data;
        }

        private DataTable GetMySQLExtensionInfo(OleDbConnection conn, string tableName)
        {
            var sql = "select COLUMN_NAME, column_comment as COMMENTS from information_schema.columns where table_name='{0}'";
            sql = string.Format(sql, tableName);
            var adapter = new OleDbDataAdapter(sql, conn);
            var data = new DataTable();
            adapter.Fill(data);
            return data;
        }
    }
}